package app

import (
	"fmt"
	"io"
	"os"

	"github.com/mars/pkg/loggers"
	"github.com/mars/third_part/fork/errors"
	"github.com/moby/term"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

const (
	progressMessage = "---->"
)

// App代表一个运行的应用.
type App struct {
	basename    string
	name        string
	description string
	options     CliOptions
	runFunc     RunFunc
	initFunc    func() // 初始化函数

	silence   bool                 // 如果你想要控制台不输出任何启动配置信息，如果不想打印配置信息，设置成true
	noVersion bool                 // 是否打印版本信息, 默认打印
	noConfig  bool                 // 是否打印配置文件信息，默认打印
	args      cobra.PositionalArgs // 对参数命令行参数校验函数

	commands []*Command

	cmd    *cobra.Command // 应用执行的命令
	cmdOut io.Writer      // 命令行输出路径，if is nil, os.Stdout will be used
}

type Option func(*App)

func WithOptions(opt CliOptions) Option {
	return func(a *App) {
		a.options = opt
	}
}

type RunFunc func(basename string) error

// WithRunFunc 配置应用运行函数，必选
func WithInitFunc(initFn func()) Option {
	return func(a *App) {
		a.initFunc = initFn
	}
}

// WithRunInit 配置应用初始化方法
func WithRunFunc(run RunFunc) Option {
	return func(a *App) {
		a.runFunc = run
	}
}

// WithDescription 添加应用的描述信息
func WithDescription(desc string) Option {
	return func(a *App) {
		a.description = desc
	}
}

// WithSilence 配置应用启动不打印相关信息，例如：版本号，配置文件等
func WithSilence() Option {
	return func(a *App) {
		a.silence = true
	}
}

// WithNoVersion 配置应用启动不设置版本
func WithNoVersion() Option {
	return func(a *App) {
		a.noVersion = true
	}
}

// WithNoConfig 设置应用启动不使用配置文件
func WithNoConfig() Option {
	return func(a *App) {
		a.noConfig = true
	}
}

// WithValidArgs 配置验证cobra参数的函数
func WithValidArgs(args cobra.PositionalArgs) Option {
	return func(a *App) {
		a.args = args
	}
}

// WithCmdOut指定命令行日志输出路径
func WithCmdOut(out io.Writer) Option {
	return func(a *App) {
		a.cmdOut = out
	}
}

// WithDefaultValidArgs 配置验证cobra参数的默认验证函数
func WithDefaultValidArgs() Option {
	return func(a *App) {
		a.args = func(cmd *cobra.Command, args []string) error {
			for _, arg := range args {
				if len(arg) > 0 {
					return fmt.Errorf("%q does not take any arguments, got %q", cmd.CommandPath(), args)
				}
			}

			return nil
		}
	}
}

// NewApp 创建一个App实例，并且指定name, basename, 以及一些可选项
// name描述应用，一个简单的名字，例如： ceph proxy
// basename是一个单词，例如: ceph-proxy
func NewApp(name string, basename string, opts ...Option) *App {
	app := &App{
		name:     name,
		basename: basename,
	}

	for _, o := range opts {
		o(app)
	}

	app.buildCommand()

	return app
}

// Run 运行cobra
func (app *App) Run() {
	if err := app.cmd.Execute(); err != nil {
		fmt.Printf("Run Application %s ERROR:  %v\n", app.name, err)
		os.Exit(1)
	}
}

// runCommand执行应用的run函数
func (app *App) runCommand(cmd *cobra.Command, args []string) error {
	if !app.noVersion {
		// display application version infomation
		PrintAndExitIfRequested()
	}

	var err error

	// flag > config
	if !app.noConfig {
		// viper中的配置会被cmd.Flags中指定的值替换
		if err = viper.BindPFlags(cmd.Flags()); err != nil {
			return err
		}
		if err = viper.Unmarshal(app.options); err != nil {
			return err
		}
	}

	if app.initFunc != nil {
		app.initFunc()
	}

	if !app.silence {
		loggers.Infof("%v Starting %s ...", progressMessage, app.name)
		if !app.noVersion {
			loggers.Infof("%v Version: `%s`", progressMessage, Get().ToJSON())
		}
		if !app.noConfig {
			loggers.Infof("%v Config file used: `%s`", progressMessage, viper.ConfigFileUsed())
		}
	}

	if app.options != nil {
		if err = app.applyOptionRules(); err != nil {
			return err
		}
	}

	// execute application run function
	if app.runFunc != nil {
		return app.runFunc(app.basename)
	}

	return nil
}

func (app *App) applyOptionRules() error {
	if completeableOptions, ok := app.options.(CompleteableOptions); ok {
		if err := completeableOptions.Complete(); err != nil {
			return err
		}
	}

	if printableOptions, ok := app.options.(PrintableOptions); ok && !app.silence {
		loggers.Infof("%v Config: `%s`", progressMessage, printableOptions.String())
	}

	if errs := app.options.Validate(); len(errs) != 0 {
		return errors.NewAggregate(errs)
	}

	return nil
}

// buildCommand 构建cobra的命令, 包含cobra命令，cobra子命令，以及命令的flag
func (app *App) buildCommand() {
	rootCmd := cobra.Command{
		Use:   FormatBaseName(app.basename),
		Short: app.name,
		Long:  app.description,
		Args:  app.args,
	}

	rootCmd.SetOut(app.cmdOut)
	rootCmd.SetErr(app.cmdOut)
	rootCmd.Flags().SortFlags = false

	initFlag(rootCmd.Flags())

	if len(app.commands) > 0 {
		for _, command := range app.commands {
			rootCmd.AddCommand(command.cobraCommand())
		}
		rootCmd.SetHelpCommand(helpCommand(FormatBaseName(app.basename)))
	}

	if app.runFunc != nil {
		rootCmd.RunE = app.runCommand
	}

	var namedFlagSets NamedFlagSets
	if app.options != nil {
		namedFlagSets = app.options.Flags()
		fs := rootCmd.Flags()
		for _, f := range namedFlagSets.FlagSets {
			fs.AddFlagSet(f)
		}
	}

	if !app.noVersion {
		AddVersionFlags(namedFlagSets.FlagSet("global"))
	}

	if !app.noConfig {
		addConfigFlag(app.basename, namedFlagSets.FlagSet("global"))
	}

	AddGlobalFlags(namedFlagSets.FlagSet("global"), rootCmd.Name())

	rootCmd.Flags().AddFlagSet(namedFlagSets.FlagSet("global"))
	addCmdTemplate(&rootCmd, namedFlagSets)

	app.cmd = &rootCmd
}

// addCmdTemplate 添加cobra， pflag的使用模板
func addCmdTemplate(cmd *cobra.Command, namedFlagSets NamedFlagSets) {
	usageFmt := "Usage:\n  %s\n"
	cols, _, _ := TerminalSize(cmd.OutOrStdout())
	cmd.SetUsageFunc(func(cmd *cobra.Command) error {
		fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine())
		PrintSections(cmd.OutOrStderr(), namedFlagSets, cols)

		return nil
	})
	cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
		fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine())
		PrintSections(cmd.OutOrStdout(), namedFlagSets, cols)
	})
}

// TerminalSize returns the current width and height of the user's terminal. If it isn't a terminal,
// nil is returned. On error, zero values are returned for width and height.
// Usually w must be the stdout of the process. Stderr won't work.
func TerminalSize(w io.Writer) (int, int, error) {
	outFd, isTerminal := term.GetFdInfo(w)
	if !isTerminal {
		return 0, 0, fmt.Errorf("given writer is no terminal")
	}
	winsize, err := term.GetWinsize(outFd)
	if err != nil {
		return 0, 0, err
	}
	return int(winsize.Width), int(winsize.Height), nil
}
